package se.kth.ict.id2203.eld;

import java.util.HashSet;
import java.util.Set;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import se.kth.ict.id2203.assignment4.Assignment4Executor;
import se.kth.ict.id2203.pp2p.PerfectPointToPointLink;
import se.kth.ict.id2203.pp2p.Pp2pSend;
import se.sics.kompics.ComponentDefinition;
import se.sics.kompics.Handler;
import se.sics.kompics.Negative;
import se.sics.kompics.Positive;
import se.sics.kompics.address.Address;
import se.sics.kompics.launch.Topology;
import se.sics.kompics.timer.ScheduleTimeout;
import se.sics.kompics.timer.Timer;


public class ELD extends ComponentDefinition {
	
	Negative<EventualLeaderDetector> eldPort = provides(EventualLeaderDetector.class);
	
	Positive<PerfectPointToPointLink> pp2p = requires(PerfectPointToPointLink.class);	
	Positive<Timer> timer = requires(Timer.class);
	
	private Address self;
	private Topology topology;
	private Set<Address> candidateSet;
	private Set<Address> neighborSet;
	private long period;
	private Address leader;

	private static final Logger logger =
			LoggerFactory.getLogger(ELD.class);
	
	public ELD() {
		logger.info("ELD created.");
		subscribe(handleInit, control);
		subscribe(handleTimeout, timer);
		subscribe(handleHeartbeatMessage, pp2p);
	}

	/*
	 * upon event ⟨ Init ⟩ do
	 */
	Handler<ELDInit> handleInit = new Handler<ELDInit>() {
		public void handle(ELDInit event) {
			
			logger.info("ELD started.");
			topology = event.getTopology();
			self = topology.getSelfAddress();
			
			candidateSet = new HashSet<Address>();
			neighborSet = topology.getNeighbors(self);
			
			leader = selectLeader();
			// send TrustMessage to leader
			trigger(new Trust(leader), eldPort);
			period = event.getPeriod();	
			
			if ( Assignment4Executor.PRINT_HEARTBEATS )
			{
				logger.info("Broadcasting HeartBeat Message...");
			}
			
			// broadcast HeartbeatMessage
			for (Address neighbor : neighborSet) {
				trigger(new Pp2pSend(neighbor, new HeartbeatMessage(self)),	pp2p);
			}
			
			// startTimer for heartbeant
			ScheduleTimeout sthb = new ScheduleTimeout(period);
			sthb.setTimeoutEvent(new EldTimeout(sthb));
			trigger(sthb, timer);
			
		}
	};
	
	
	/*
	 * 	11: upon event ⟨ T imeout ⟩ do
		12: newleader = select(candidateset);
		13: if (leader ̸= newleader ∧ newleader ̸= NIL) then
		14: period := period + ∆;
		15: leader := newleader;
		16: trigger ⟨ trust | leader ⟩;
		17: end if
		18: for all pi ∈ Π do
		19: trigger ⟨ pp2pSend | pi , [Heartbeat] ⟩;
		20: end for
		21: candidateset := ∅;
		22: startTimer(period);
		23: end event
	 */
	Handler<EldTimeout> handleTimeout = new Handler<EldTimeout>() {
		public void handle(EldTimeout event) {
			
			Address newLeader = selectLeader();
			
			if ( newLeader != null )
			{
				if ( leader.getId() != newLeader.getId() ) 
				{
					// Delta = current period
					period = period + period;
					leader = newLeader;
					trigger(new Trust(leader), eldPort);
				}
			}
			
			if ( Assignment4Executor.PRINT_HEARTBEATS )
			{
				logger.info("Broadcasting HeartBeat Message...");
			}

			// broadcast HeartbeatMessage
			for (Address neighbor : neighborSet) {
				trigger(new Pp2pSend(neighbor, new HeartbeatMessage(self)),	pp2p);
			}
			
			candidateSet.clear();
			
			ScheduleTimeout sthb = new ScheduleTimeout(period);
			sthb.setTimeoutEvent(new EldTimeout(sthb));
			trigger(sthb, timer);
			
		}
	};
	
	/*
	 * Choose leader by minimum rank
	 */
	public Address selectLeader()
	{
		Address leaderProposal = self;
		for (Address neighbor : candidateSet) {
			
			// If neighbour has lower id than current leader
			if ( leaderProposal.getId() > neighbor.getId()  )
			{
				leaderProposal = neighbor;
			}

		}
		
		return leaderProposal;
	}
	
	/*
	 * 	24: upon event ⟨ pp2pDeliver | pj , [Heartbeat] ⟩ do
		25:	candidateset := candidateset ∪ {pj };
		26: end event
	 */
	Handler<HeartbeatMessage> handleHeartbeatMessage = new Handler<HeartbeatMessage>() {
		public void handle(HeartbeatMessage event) {
			// handle HeartbeatMessage
			Address src = event.getSource();
			if (!candidateSet.contains(src)) {
				candidateSet.add(src);
			}
			
			if ( Assignment4Executor.PRINT_HEARTBEATS )
			{
				logger.info("HeartBeat Message from " + src);
			}
			
		}
	};
	
}
